/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.relations.restore;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.calculables.CalculableHolder;
import org.unidata.mdm.core.type.calculables.ModificationBox;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.RelationRestoreContext;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.RelationComposerComponent;
import org.unidata.mdm.data.type.calculables.impl.RelationRecordHolder;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.OriginRelationInfoSection;
import org.unidata.mdm.data.type.data.impl.OriginRelationImpl;
import org.unidata.mdm.data.type.keys.RelationEtalonKey;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.data.type.keys.RelationOriginKey;
import org.unidata.mdm.data.type.timeline.RelationTimeline;
import org.unidata.mdm.system.service.PlatformConfiguration;
import org.unidata.mdm.system.type.pipeline.Point;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;
import org.unidata.mdm.system.type.support.IdentityHashSet;

/**
 * @author Mikhail Mikhailov on Nov 10, 2019
 */
@Component(RelationRestoreTimelineExecutor.SEGMENT_ID)
public class RelationRestoreTimelineExecutor extends Point<RelationRestoreContext> {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_RESTORE_TIMELINE]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relation.restore.timeline.description";
    /**
     * The composer.
     */
    @Autowired
    private RelationComposerComponent relationComposerComponent;
    /**
     * Version numbers.
     */
    @Autowired
    private PlatformConfiguration platformConfiguration;
    /**
     * Constructor.
     * @param id
     * @param description
     */
    public RelationRestoreTimelineExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void point(RelationRestoreContext ctx) {

        MeasurementPoint.start();
        try {

            // Create next timeline to master timeline
            RelationKeys keys = ctx.relationKeys();
            Date ts = ctx.timestamp();
            String user = SecurityUtils.getCurrentUserName();

            Timeline<OriginRelation> current = ctx.currentTimeline();
            Timeline<OriginRelation> next = null;
            ModificationBox<OriginRelation> box = ctx.modificationBox();

            // Period
            if (ctx.isPeriodRestore()) {

                // 'current' is expected to be already restricted to period bounds
                Set<CalculableHolder<OriginRelation>> revisions = new IdentityHashSet<>();
                List<CalculableHolder<OriginRelation>> result = new ArrayList<>(current.size());

                current
                    .selectBy(ctx.getValidFrom(), ctx.getValidTo())
                    .forEach(i -> {

                        List<CalculableHolder<OriginRelation>> existing = i.toList();
                        existing.stream()
                                .filter(v -> v.getStatus() == RecordStatus.INACTIVE)
                                .map(v -> {

                                    // Sort out revisions, which were already added.
                                    if (revisions.contains(v)) {
                                        return null;
                                    }

                                    revisions.add(v);

                                    RelationOriginKey rok = (RelationOriginKey) v.getOriginKey();
                                    return new OriginRelationImpl()
                                        .withDataRecord(v.getValue())
                                        .withInfoSection(new OriginRelationInfoSection()
                                            .withRelationOriginKey(rok)
                                            .withRelationType(keys.getRelationType())
                                            .withRelationName(keys.getRelationName())
                                            .withFromEntityName(keys.getFromEntityName())
                                            .withToEntityName(keys.getToEntityName())
                                            .withValidFrom(i.getValidFrom())
                                            .withValidTo(i.getValidTo())
                                            .withCreateDate(rok.getCreateDate())
                                            .withCreatedBy(rok.getCreatedBy())
                                            .withUpdateDate(ts)
                                            .withUpdatedBy(user)
                                            .withRevision(0)
                                            .withStatus(RecordStatus.ACTIVE)
                                            .withOperationType(OperationType.DIRECT)
                                            .withShift(v.getValue().getInfoSection().getShift())
                                            .withMajor(platformConfiguration.getPlatformMajor())
                                            .withMinor(platformConfiguration.getPlatformMinor()));
                                })
                                .filter(Objects::nonNull)
                                .map(RelationRecordHolder::new)
                                .collect(Collectors.toCollection(() -> result));
                });

                // Original keys traditionally remain unchanged until the end of the PL
                // Set the new keys state to the NEXT timeline
                RelationKeys nextKeys = RelationKeys.builder(keys)
                    .updateDate(ts)
                    .updatedBy(user)
                    .build();

                if (Objects.nonNull(box)) {
                    box.toModifications().forEach((k, v) -> v.forEach(result::add));
                }

                next = new RelationTimeline(keys, result);
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i));

            // Record
            } else {

                // Original keys traditionally remain unchanged until the end of the PL
                // Set the new keys state to the NEXT timeline
                RelationKeys nextKeys = RelationKeys.builder(keys)
                    .etalonKey(RelationEtalonKey.builder(keys.getEtalonKey())
                        .status(RecordStatus.ACTIVE)
                        .build())
                    .updateDate(ts)
                    .updatedBy(user)
                    .build();

                List<CalculableHolder<OriginRelation>> target = new ArrayList<>(current.getCalculables());
                if (Objects.nonNull(box)) {
                    box.toModifications().forEach((k, v) -> v.forEach(target::add));
                }

                next = new RelationTimeline(nextKeys, target);
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i));
            }

            ctx.nextTimeline(next);

        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return RelationRestoreContext.class.isAssignableFrom(start.getInputTypeClass());
    }
}
